package estadistec.logica.tabla;

import java.util.Hashtable;

import estadistec.logica.dependencias.GestorNodos;
import estadistec.logica.dependencias.Nodo;

import estadistec.logica.tipos.TipoValor;
import estadistec.logica.parser.Parser;

import estadistec.logica.Constantes;
import estadistec.logica.gestores.Gestor;

import estadistec.grafico.GTabla;

import estadistec.excepciones.ExcepcionLlaveInexistente;
import estadistec.excepciones.ExcepcionCapacidadExcedida;
import estadistec.excepciones.ExcepcionCapacidadNegativa;
import estadistec.logica.parser.IEvaluable;
import java.util.ArrayList;

/**
 * Esta clase representa a una Tabla. En ella se manejan las 
 * propiedades de la misma y gestiona sus atributos. Esta es la 
 * única forma de comunicación que tiene la interfaz gráfica con los 
 * atributos.
 */
abstract public class LTabla extends GestorNodos<Atributo> {

	/** 
	 * Gestor de Tablas que gestiona a la tabla actual.
	 */
	private GestorTablas _miGestor;

	/**
	 * Constructor de la clase. Al crearse una tabla en la interfaz 
	 * gráfica, se llama este método de manera inmediata. Se llama
	 * mediante el GestorTablas, y este por Escritorio.
	 */
	public LTabla(Long llave, String nombre, GestorTablas gestorTablas) {
		this._manejables = new Hashtable<Long, Atributo>();
		this._llave = llave;
		this._miGestor = gestorTablas;
		this._nombre = nombre;
	}

	/** 
	 * Variable que guarda el número de casos que hay en la tabla.
	 */
	protected int _noCasos;

	/** 
	 * Devuelve el número de casos de la tabla.
	 */
	public int getNoCasos() {
		return this._noCasos;
	}

	/** 
	 * Devuelve el tipo de tabla. Depende de cada una de las clases
	 * que heredan.
	 */
	abstract public TipoTabla getTipo();

	/** 
	 * Interfaz gráfica de esta tabla.
	 */
	protected GTabla _gTabla;

	/** 
	 * Define quién es la interfaz gráfica de la tabla.
	 * Se debería definir una única vez, y como es al momento de
	 * crearse, se envia "hacia adelante".
	 */
	public void setGTabla(GTabla gTabla) {
		assert(this._gTabla == null);
		this._gTabla = gTabla;
		try {
			this._gTabla.setSelected(true);
		}
		catch (java.beans.PropertyVetoException e) {
			assert(false);
		}
	}

	/** 
	 * Devuelve el puntero a la tabla gráfica.
	 */
	public GTabla getGTabla() {
		return this._gTabla;
	}

	/** 
	 * Devuelve el máximo número de casos que es permitido insertar.
	 * Debe preguntarse <em>siempre</em>, antes de insertar cualquier 
	 * número de casos, aunque solo se inserte 1.
	 */
	public int maxNoCasosInsertar() 
	{
		return Constantes.MAX_NO_CASOS - this._noCasos;
	}

	/** 
	 * Agrega casos a la tabla. Antes de solicitar agregar casos a 
	 * la tabla, siempre debe verificarse que el número de casos a 
	 * agregar no exceda el valor dado por el método 
	 * {@link #maxNoCasosInsertar}.
	 * 
	 * @param indice Caso 1-indexado a partir del cual se van a 
	 * agregar el número de casos descrito por el usuario. Si se 
	 * quieren insertar al inicio se pone 0. Es decir, se insertan 
	 * "debajo" del índice especificado.
	 *
	 * @param noCasos Número de casos a agregar. Ya se ha verificado 
	 * que el número de casos no va a generar ningún error.
	 *
	 * @todo Recordar que hay que hacer un árbol para las actualizaciones,
	 * para no actualizar un atributo varias veces.
	 */
	public void insertarCasos(int indice, int noCasos)
	{
		// Se agregan los casos a todos los atributos.
		for (Atributo atributo : this._manejables.values())
			atributo.insertarCasos(indice, noCasos);
		this._noCasos += noCasos;
		// Se actualizan todos los atributos.
		for (Atributo atributo : this._manejables.values())
			if (!atributo.esIndependiente())
				atributo.actualizar();
	}


	/** 
	 * Se eliminan casos de la tabla. El usuario lo hace mediante
	 * la selección de casos y solicitando que se eliminen. No puede
	 * haber error de usuario.
	 *
	 * El método consiste en ir eliminando casos en cada uno de los 
	 * atributos que se encuentren en la tabla.
	 * 
	 * @param indice Caso inicial que se desea eliminar inclusive. 
	 * 1-indexado.
	 *
	 * @param noCasos Número de casos que se van a eliminar.
	 */
	public void eliminarCasos (int indice, int noCasos)
	{
		System.out.println("LTabla -> Antes de eliminar: " + noCasos);
		assert(this._manejables != null);
		for (Atributo atributo : this._manejables.values())
			atributo.eliminarCasos(indice - 1, noCasos);
		this._noCasos -= noCasos;
		System.out.println("LTabla -> Despues de eliminar: " + noCasos);
	}


	/**
	 * Crea un atributo. Existen dos formas distintas en las cuales 
	 * el usuario puede crear un atributo:
	 * 	-# Mediante una fórmula. El usuario ingresa una fórmula, así 
	 * 	que al momento de revisar si la fórmula es o no la correcta, 
	 * 	se obtiene el tipo de atributo.
	 * 	-# De manera explícita. El usuario define el tipo de 
	 * 	atributo. La otra información requerida es el número de 
	 * 	casos, los cuales se toman del número de casos existente en 
	 * 	la tabla. Dado que el número de casos se toman a partir de 
	 * 	los casos de la tabla, no puede haber error por parte del 
	 * 	usuario.
	 * 
	 * @param tipo Tipo de atributo. {@link estadistec.logica.tipos.TipoValor}.
	 *            
	 * @return La llave del nuevo atributo.
	 *
	 * @throws ExcepcionCapacidadExcedida El número de casos excede 
	 * el máximo.
	 *
	 * @throws ExcepcionCapacidadNegativa El número de casos es 
	 * negativo.
	 */
	public Long crearAtributo(TipoValor tipo, String nombre) 
		throws ExcepcionCapacidadNegativa, ExcepcionCapacidadExcedida
	{
		Long llave = getNuevaLlave();
		Atributo nuevoAtributo = new Atributo(tipo, nombre, this._noCasos, this, llave);
		
		this._manejables.put(llave, nuevoAtributo);

		return llave;
	}

	/** 
	 * Método que informa si el atributo tiene dependientes. Es el 
	 * inicio del proceso en caso de que se quiera eliminar un 
	 * atributo: 
	 * 	-# Primero se debe preguntar si tiene o no dependientes. 
	 * 	-# En caso de que las tenga, debe preguntar al usuario si 
	 * 	desea eliminar el atributo.
	 * 	-# El paso 2 es @see eliminarDependientes si el usuario así i
	 * 	lo aceptó.
	 * 	-# El paso final es eliminar el atributo.
	 * 
	 * @param llave Identificación del atributo por la cual se 
	 * pregunta.
	 * 
	 * @return <em>True</em> si el atributo tiene dependientes, y 
	 * <em>False</em> en caso de que no tenga dependientes.
	 */
	public boolean tieneDependientes (Long llave) 
	{
		assert (this._manejables.containsKey(llave));
		return this._manejables.get(llave).tieneDependientes();
	}

	/**
	 * Elimina un atributo. Deber haber verificado que no tenía
	 * dependientes, y si los tenía, debió haberlos eliminado.
	 *
	 * @param llave
	 *            Identificador del atributo a eliminar.
	 */
	public void eliminarAtributo(Long llave) {
		assert (this._manejables.containsKey(llave));
		assert (!this._manejables.get(llave).tieneDependientes());
		this._manejables.get(llave).independizate();
		this._manejables.remove(llave);
		liberarLlave(llave);
	}
	
	
	/**
	 * Devuelve el objeto atributo correspondiente a la llave dada.
	 * @param llave Id del atributo
	 * @return el objeto Atributo
	 * @throws ExcepcionLlaveInexistente en caso de que la llave no 
	 * corresponde a ningún atributo
	 */
	public Atributo getAtributo(Long llave) throws ExcepcionLlaveInexistente
	{
		if(!this._manejables.containsKey(llave))
			throw new ExcepcionLlaveInexistente("LTabla -> getAtributo");
		
		return (this._manejables.get(llave));
	}

	/** 
	 * Método que busca un nodo según una cadena de nombres.
	 * 
	 * @param nombres Nombres de los padres y Nodo.
	 * @param nivel Nivel actual en los nombres.
	 * 
	 * @return Nodo que se busca o {@code null} si no está.
	 */
	public Nodo buscarRaiz(String[] nombres, int nivel) {
		if (nivel == 0)
			return (this.buscarNodo(nombres, nivel));
		return (_miGestor.buscarNodo(nombres, nivel - 1));
	}

	/** 
	 * Método que busca un nodo según una cadena de nombres. 
	 * 
	 * @param nombres Nombres de los padres y Nodo.
	 * @param nivel Nivel actual en los nombres.
	 * 
	 * @return Nodo que se busca o {@code null} si no está.
	 */
	public Nodo buscarNodo(String[] nombres, int nivel) {
		// Se debería de haber llegado al final.
		assert (nivel == nombres.length - 1);

		// Buscando y devolviendo el nodo.
		return (getManejableXNombre(nombres[nivel]));
	}

	/** 
	 * Analizar un nombre que están proponiendo para "Mi".
	 * 
	 * @param nombre Nombre propuesto.
	 * 
	 * @return El tipo de variable al que corresponde el nombre.
	 * En realidad el único tipo válido seria INDEPENDIENTE.
	 */
	public boolean verificarNombreTablaSinRepetir (String nombre) {
		return (this._miGestor.existeManejableXNombre(nombre) == false);
	}

	 /**
	  * Solicitud de creación de un atributo booleano por la tabla gráfica.
	  */
	 public void agregarAtributoBooleano(String nombre) 
	 {
		 Long llaveAtributoNuevo = this.crearAtributo(TipoValor.BOOLEANO, nombre);
		 this._gTabla.agregarAtributoGrafico(nombre, llaveAtributoNuevo, new String[0]);
	 }

	 public void agregarAtributoBooleano(String nombre, Parser formula, String formulaUsuario)
	 {
		 Atributo temp = this._manejables.get(this.crearAtributo(TipoValor.BOOLEANO, nombre));
		 temp.setTextoFormula(formulaUsuario);
		 String[] txtValores = temp.setFormula(formula.getEstructura());
		 ArrayList<IEvaluable> deQuienesDependo = formula.getListaSuperiores();
		 Nodo actual = (Nodo) temp;
		 for(IEvaluable e : deQuienesDependo)
		 {
			 Nodo superior = (Nodo) e;
			 superior.agregarDependiente(actual);
			 actual.agregarSuperior(superior);
		 }
		 this._gTabla.agregarAtributoGrafico(nombre, temp.getLlave(), txtValores);
	 }

	 /**
	  * Solicitud de creación de un atributo entero por la tabla gráfica.
	  */
	 public void agregarAtributoEntero(String nombre) 
	 {
		 Long llaveAtributoNuevo = this.crearAtributo(TipoValor.ENTERO, nombre);
		 this._gTabla.agregarAtributoGrafico(nombre, llaveAtributoNuevo, new String[0]);
	 }

	 public void agregarAtributoEntero(String nombre, Parser formula, String formulaUsuario)
	 {
		 Atributo temp = this._manejables.get(this.crearAtributo(TipoValor.ENTERO, nombre));
		 temp.setTextoFormula(formulaUsuario);
		 String[] txtValores = temp.setFormula(formula.getEstructura());
		 ArrayList<IEvaluable> deQuienesDependo = formula.getListaSuperiores();
		 Nodo actual = (Nodo) temp;
		 for(IEvaluable e : deQuienesDependo)
		 {
			 Nodo superior = (Nodo) e;
			 superior.agregarDependiente(actual);
			 actual.agregarSuperior(superior);
		 }
		 this._gTabla.agregarAtributoGrafico(nombre, temp.getLlave(), txtValores);
	 }

	 /**
	  * Solicitud de creación de un atributo flotante por la tabla gráfica.
	  */
	 public void agregarAtributoFlotante(String nombre, int noDecimales) 
	 {
		 Long llaveAtributoNuevo = this.crearAtributo(TipoValor.FLOTANTE, nombre);
		 this._manejables.get(llaveAtributoNuevo).setNoDecimales(noDecimales);
		 this._gTabla.agregarAtributoGrafico(nombre, llaveAtributoNuevo, new String[0]);
	 }

	 public void agregarAtributoFlotante(String nombre, int noDecimales, Parser formula, String formulaUsuario)
	 {
		 Long llave = this.crearAtributo(TipoValor.FLOTANTE, nombre);
		 Atributo temp =  this._manejables.get(llave);
		 temp.setTextoFormula(formulaUsuario);
		 temp.setNoDecimales(noDecimales);
		 String[] txtValores = temp.setFormula(formula.getEstructura());
		 ArrayList<IEvaluable> deQuienesDependo = formula.getListaSuperiores();
		 Nodo actual = (Nodo) temp;
		 for(IEvaluable e : deQuienesDependo)
		 {
			 Nodo superior = (Nodo) e;
			 superior.agregarDependiente(actual);
			 actual.agregarSuperior(superior);
		 }
		 
		 this._gTabla.agregarAtributoGrafico(nombre, temp.getLlave(), txtValores);
	 }

	 /**
	  * Solicitud de creación de un atributo de tipo intervalo por la tabla gráfica.
	  */
	 public void agregarAtributoIntervalo(String nombre, int noDecimales) 
	 {
		 Long llaveAtributoNuevo = this.crearAtributo(TipoValor.INTERVALO, nombre);
		 this._manejables.get(llaveAtributoNuevo).setNoDecimales(noDecimales);
		 this._gTabla.agregarAtributoGrafico(nombre, llaveAtributoNuevo, new String[0]);
	 }

	 public void agregarAtributoIntervalo(String nombre, int noDecimales, Parser formula, String formulaUsuario)
	 {
		 Atributo temp = 
			 this._manejables.get(this.crearAtributo(TipoValor.INTERVALO, nombre));
		 temp.setTextoFormula(formulaUsuario);
		 temp.setNoDecimales(noDecimales);
		 String[] txtValores = temp.setFormula(formula.getEstructura());
		 ArrayList<IEvaluable> deQuienesDependo = formula.getListaSuperiores();
		 Nodo actual = (Nodo) temp;
		 for(IEvaluable e : deQuienesDependo)
		 {
			 Nodo superior = (Nodo) e;
			 superior.agregarDependiente(actual);
			 actual.agregarSuperior(superior);
		 }
		 this._gTabla.agregarAtributoGrafico(nombre, temp.getLlave(), txtValores);
	 }

	 /**
	  * Solicitud de creación de un atributo cualitativo por la tabla gráfica.
	  */
	 public void agregarAtributoCualitativo(String nombre, String[] espacioMuestral) 
	 {
		 Long llaveAtributoNuevo = this.crearAtributo(TipoValor.CUALITATIVO, nombre);
		 this._gTabla.agregarAtributoGrafico(nombre, llaveAtributoNuevo, new String[0]);
	 }

	 public void agregarAtributoCualitativo(String nombre, Parser formula, String formulaUsuario)
	 {
		 Atributo temp = this._manejables.get(this.crearAtributo(TipoValor.CUALITATIVO, nombre));
		 temp.setTextoFormula(formulaUsuario);
		 String[] txtValores = temp.setFormula(formula.getEstructura());
		 ArrayList<IEvaluable> deQuienesDependo = formula.getListaSuperiores();
		 Nodo actual = (Nodo) temp;
		 for(IEvaluable e : deQuienesDependo)
		 {
			 Nodo superior = (Nodo) e;
			 superior.agregarDependiente(actual);
			 actual.agregarSuperior(superior);
		 }
		 this._gTabla.agregarAtributoGrafico(nombre, temp.getLlave(), txtValores);
	 }

	 /** 
	  * Informa si el atributo tiene una fórmula. Cuando el usuario modifica una celda, 
	  * primero debe verificarse si el atributo es o no independiente. En caso de que
	  * devuelva false, se le debe consultar al usuario si desea eliminar la fórmula.
	  * Si devuelve true, el atributo se puede modificar. Se debe llamar este método
	  * CADA VEZ que se quiera modificar el atributo.
	  * 
	  * @param llaveAtributo Llave del atributo respectivo.
	  *
	  * @throws LlaveInexistente En caso de que la llave del atributo no se encuentre
	  * entre los manejables.
	  */
	 public boolean esAtributoIndependiente(Long llaveAtributo) 
		 throws ExcepcionLlaveInexistente
	 {
		 if (!this._manejables.containsKey(llaveAtributo))
			 throw new ExcepcionLlaveInexistente("Llave : " + llaveAtributo);
		 return (this._manejables.get(llaveAtributo).esIndependiente());
	 }

	 /**
	  * Independiza el atributo de sus superiores, en caso de que el usuario 
	  * así lo solicite, o la fórmula deje de ser válida. Los dependientes
	  * del atributo siguen dependieno de él.
	  *
	  * @throws ExcepcionLlaveInexistente En caso de que el atributo no se
	  * encuentre.
	  */
	 public void independizarAtributo(Long llaveAtributo) 
		 throws ExcepcionLlaveInexistente
	 {
		 if (!this._manejables.containsKey(llaveAtributo))
			 throw new ExcepcionLlaveInexistente("Llave : " + llaveAtributo);

		 assert (this._manejables.get(llaveAtributo).tieneDependientes());
		 Atributo atrTemp = this._manejables.get(llaveAtributo);
		 atrTemp.independizate();
		 atrTemp.deleteFormula();
	 }

	 /**
	  * Devuelve el texto de una celda de un atributo. Útil para cuando
	  * en caso de que se haya ingresado un valor incorrecto o cancelado
	  * una acción en la tabla gráfica, se pueda obtener el texto de
	  * la celda y volver a colocar el valor que había.
	  *
	  * @throws ExcepcionLlaveInexistente En caso de que el atributo no se
	  * encuentre.
	  */
	 public String getTextoValor(Long llaveAtributo, int indice) 
		 throws ExcepcionLlaveInexistente
	 {
		 if (!this._manejables.containsKey(llaveAtributo))
			 throw new ExcepcionLlaveInexistente("Llave : " + llaveAtributo);
		 return this._manejables.get(llaveAtributo).getTextoValor(indice - 1);
	 };

	 /**
	  * Modifica el valor de una celda en un atributo. Ya se ha verificado que el
	  * atributo es independiente. Si el valor es incorrecto devuelve null,
	  * y si no devuelve el String que corresponde al String del valor respectivo.
	  */
	 public String setValor(Long llaveAtributo, String valor, int indice) 
	 {
		if (!this._manejables.containsKey(llaveAtributo))
			 throw new ExcepcionLlaveInexistente ("Llave : " + llaveAtributo);

		Atributo atrTemp = this._manejables.get(llaveAtributo);
		String nuevoTextoValor = atrTemp.set(indice - 1, valor);
		atrTemp.actualizarDependientes();
		return nuevoTextoValor;
	 }

	 /**
	  * Devuelve el tipo de atributo. Se debería de utilizar después de tratar
	  * de definir un valor, de manera que cuando el valor esté incorrecto, se
	  * informe al usuario que el valor debe ser de tipo tal...
	  */
	 public TipoValor getTipoAtributo(Long llaveAtributo) 
		 throws ExcepcionLlaveInexistente
	 {
		 if (!this._manejables.containsKey(llaveAtributo))
			 throw new ExcepcionLlaveInexistente("Llave : " + llaveAtributo);

		 return (this._manejables.get(llaveAtributo).getTipo());
	 };

	 /** 
	  * Método requerido por la interfaz IManejable.
	  */
	 public Gestor getGestor() {
		 return this._miGestor;
	 }

	 /**
	  * Método que permite saber si la tabla tiene dependientes externos.
	  * Por ejemplo, si algún atributo es un superior de uno nodo externo
	  * a la tabla.
	  */
	 public boolean tieneDependientes() {
		 for (Atributo atributo : this._manejables.values())
			 if (atributo.tieneDependientesExternos())
				 return true;
		 return false;
	 };

	 /** 
	  * Independiza cada uno de los atributos.
	  */
	 public void independizate() {
		 for (Atributo atributo : this._manejables.values())
			 if (!atributo.esIndependiente())
				 atributo.independizate();
	 }

	 /**
	  * Libera a cada uno de los dependientes <em>externos</em> de TODOS 
	  * los atributos.
	  */
	 public void liberarDependientes() {
		 for (Atributo atributo : this._manejables.values())
			 if (atributo.tieneDependientes())
				 atributo.liberarDependientes();
	 }

	 /** 
	  * Libera a los dependientes del atributo especificado.
	  */
	 public void liberarDependientes(Long llave) 
	 {
		 assert (this._manejables.containsKey(llave));
	 	 Atributo atributo = this._manejables.get(llave);
		 assert (atributo.tieneDependientes());
		 atributo.liberarDependientes();
	 }

	 /** 
	  * Devuelve el número de atributos de la tabla.
	  */
	 public int getNoAtributos() {
		 return (this._manejables.size());
	 }

	 /**
	  * Solicitud para la GTabla de que actualice un atributo o una medida.
	  */
	 public void actualizarValores(Long llave, String[] valores) {
		 this._gTabla.actualizarValores(llave, valores);
	 }

}
